﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Valentia.Kinection.DataTypes;
using Valentia.Kinection.Configuration;
using SuperWebSocket;
using SuperSocket.SocketBase;

namespace Valentia.Kinection.Service
{
    #region ConnectionStatus Enum
    /// <summary>
    ///  Service client connection status
    /// </summary>
    internal enum ConnectionStatus 
    { 
        /// <summary>
        /// Client connecting
        /// </summary>
        Connecting = 1, 

        /// <summary>
        /// Client connected
        /// </summary>
        Connected, 

        /// <summary>
        /// Client disconnected
        /// </summary>
        Disconnected
    };
    #endregion

    /// <summary>
    /// Data user client representation at server end
    /// </summary>
    internal class WebSocketClients : IDisposable
    {
        #region DATA MEMBERS
        /// <summary>
        /// Used for locking purpose to synchronize the connecting and closing process.
        /// </summary>
        private readonly Object _CloseSync = new Object();

        /// <summary>
        /// Web socket server
        /// </summary>
        private WebSocketServerWrapper serverWrapper;

        /// <summary>
        /// Service client session
        /// </summary>
        private WebSocketSession session;

        /// <summary>
        /// Service clients messages queue
        /// </summary>
        private Queue<String> messagesQueue;

        /// <summary>
        /// Messages processor thread
        /// </summary>
        private Thread receivedMessageThread;
        #endregion

        #region PROPERTIES
        /// <summary>
        /// Client connection status
        /// </summary>
        internal ConnectionStatus ConnectionStatus { get; set; }
        
        /// <summary>
        /// Is secured
        /// </summary>
        internal bool Secured { get; private set; }

        /// <summary>
        /// Is authenticated service client to receive video stream
        /// </summary>
        internal bool VideoStreamAllowed { get; private set; }

        /// <summary>
        /// Is authenticated service client to receive depth stream 
        /// </summary>
        internal bool DepthStreamAllowed { get; private set; }

        /// <summary>
        /// Is authenticated service client to receive skeleton data
        /// </summary>
        internal bool SkeletonDataAllowed { get; private set; }

        /// <summary>
        /// Allowed joint id's data
        /// </summary>
        internal SkeletonJointID[] SkeletonJointsToSend { get; set; }

        /// <summary>
        ///  Service client remote end point
        /// </summary>
        internal String RemoteEndPoint { get; private set; }
        
        /// <summary>
        /// Service client user name
        /// </summary>
        internal String UserName { get; private set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="serverWrapper">Web Socket Server</param>
        /// <param name="session">Client session</param>
        /// <param name="secured">Is secured</param>
        internal WebSocketClients(WebSocketServerWrapper serverWrapper, WebSocketSession session
            , bool secured)
        {
            this.serverWrapper = serverWrapper;
            this.session = session;
            this.RemoteEndPoint = session.RemoteEndPoint.ToString();
            this.messagesQueue = new Queue<String>();

            this.ConnectionStatus = ConnectionStatus.Connecting;
            this.Secured = secured;
            this.VideoStreamAllowed = false;
            this.DepthStreamAllowed = false;
            this.SkeletonDataAllowed = true;
            this.UserName = String.Empty;
            
            // Loading all the joints except Count

            List<SkeletonJointID> joints = new List<SkeletonJointID>();
            foreach (String jointId in Enum.GetNames(typeof(SkeletonJointID)))
            {
                SkeletonJointID id = (SkeletonJointID)(Enum.Parse(typeof(SkeletonJointID), jointId, true));
                if (id == SkeletonJointID.Count) continue;

                joints.Add((SkeletonJointID)(Enum.Parse(typeof(SkeletonJointID), jointId, true)));
            }
            this.SkeletonJointsToSend = joints.ToArray();

            #region Initializing thread to process service client messages queue

            this.receivedMessageThread = new Thread(this.DequeueAndProcessMessage);
            this.receivedMessageThread.IsBackground = true;
            this.receivedMessageThread.Priority = ThreadPriority.Highest;
            this.receivedMessageThread.Start();

            #endregion

            // Launching thread to wait for client authentication
            ThreadPool.QueueUserWorkItem(this.AuthRequestWaitThred);
        }
        #endregion

        #region THREADS
        /// <summary>
        /// Waits for authentication from client for a specified time of period
        /// if client failed to authenticate, client session will expire.
        /// </summary>
        /// <param name="state">State</param>
        private void AuthRequestWaitThred(object state)
        {
            try
            {
                int attemptCount = 0, maxCount = 12;

                while (attemptCount < maxCount)
                {
                    try
                    {
                        if (this.ConnectionStatus != ConnectionStatus.Connecting) return;
                    }
                    finally
                    {
                        attemptCount++;
                        if (attemptCount < maxCount) { Thread.Sleep(5000); }
                    }
                }
            }
            catch (Exception exp) { this.Log(exp, "Exception in auth. request wait thread: {0}", exp.Message); }
            finally
            {
                try
                {
                    lock (this._CloseSync)
                    {
                        if (this.ConnectionStatus == ConnectionStatus.Connecting)
                        {
                            this.Close("Auth. request not received from client");
                        }
                    }
                }
                catch { }
            }
        }

        /// <summary>
        /// Dequeue the messages and process messages queue
        /// </summary>
        private void DequeueAndProcessMessage()
        {
            try
            {
                String message = String.Empty;
                while (true)
                {
                    try
                    {
                        if (this.ConnectionStatus == ConnectionStatus.Disconnected) return;

                        lock (this.messagesQueue)
                        {
                            if (this.messagesQueue.Count <= 0) continue;

                            message = (this.messagesQueue.Dequeue() ?? String.Empty).Trim();
                        }

                        if (message.Length <= 0) continue;

                        this.Log("Message received: {0}", message);
                        this.ProcessMessage(message);
                        message = String.Empty;
                    }
                    catch (Exception exp) { this.Log(exp, "Exception in Dequeue message thread: {0}", exp.Message); }
                    finally { if (this.ConnectionStatus != ConnectionStatus.Disconnected) { Thread.Sleep(50); } }
                }
            }
            catch (Exception exp)
            {
                if (this.ConnectionStatus != ConnectionStatus.Disconnected)
                {
                    this.Log(exp, "Exception in dequeue mesage thread. Aborting abnormally: {0}.", exp.Message);
                    this.Close("Dequeue mesage thread aborted abnormally");
                }
            }
            finally { try { Thread.CurrentThread.Abort(); } catch { } }
        }
        #endregion

        #region PRIVATE METHODS
        /// <summary>
        /// Processes the message according to message type
        /// </summary>
        /// <param name="message">message data</param>
        private void ProcessMessage(String message)
        {
            try
            {
                MessageBase messageBase = MessageBase.DecodeString(message);
                switch (messageBase.MessageType)
                {
                    #region AUTH. REQUEST
                    case MessageType.AuthRequest:
                        {
                            AuthRequest authReq = messageBase as AuthRequest;
                            lock (this._CloseSync)
                            {
                                try
                                {
                                    if (this.ConnectionStatus != ConnectionStatus.Connecting) return;

                                    this.UserName = authReq.Login;
                                    this.Log("Auth. request received. Login: {0}, Pwd: {1}", authReq.Login, authReq.Password);

                                    #region Authenticating service client

                                    String error = String.Empty;
                                    DataUser dataUser = ConfigurationHelper.GetDataUserByLoginName(this.UserName);
                                    if (dataUser == null)
                                    {
                                        this.SendMessage(new AuthReply() { Status = false, Reason = "User name or password is incorrect" }, out error);
                                        this.Close(String.Format("No data user with login: {0} exist in database", this.UserName));
                                        return;
                                    }

                                    
                                    if (String.Compare(dataUser.Password, authReq.Password, false) != 0)
                                    {
                                        this.SendMessage(new AuthReply() { Status = false, Reason = "User name or password is incorrect" }, out error);
                                        this.Close(
                                            String.Format(
                                            "User: {0} exists but password provided is wrong. Provided Pwd: {1}. Actual Pwd: {2}",
                                            this.UserName, authReq.Password, dataUser.Password));
                                        return;
                                    }

                                    #endregion

                                    #region Setting the allowed stream data and joints data

                                    this.VideoStreamAllowed = dataUser.SendVideoFrame;
                                    this.DepthStreamAllowed = dataUser.SendDepthFrame;
                                    this.SkeletonDataAllowed = dataUser.SendSkeletonFrame;

                                    List<DataUserAllowedSkeletonJoint> allowedJoints =
                                        ConfigurationHelper.GetDataUserAllowedSkeletonJointsByDataUserID(dataUser.ID);

                                    if (allowedJoints == null || allowedJoints.Count <= 0) { this.SkeletonJointsToSend = new SkeletonJointID[0]; }
                                    else
                                    {
                                        List<SkeletonJointID> allowedJointIDs = new List<SkeletonJointID>(allowedJoints.Count);
                                        foreach (DataUserAllowedSkeletonJoint allowedSktID in allowedJoints)
                                        {
                                            if (allowedSktID.SkeletonJointIDEnm == SkeletonJointID.Count) continue;

                                            allowedJointIDs.Add(allowedSktID.SkeletonJointIDEnm);
                                        }
                                        this.SkeletonJointsToSend = allowedJointIDs.ToArray();
                                    }

                                    #endregion

                                    if (!this.serverWrapper.OnClientAuthenticated(this, out error))
                                    {
                                        String sendingAuthReplyError = String.Empty;
                                        this.SendMessage(new AuthReply() { Status = false, Reason = error }, out sendingAuthReplyError);
                                        this.Close(error);

                                        return;
                                    }

                                    this.ConnectionStatus = ConnectionStatus.Connected;
                                    this.Log("Client authenticated successfully");

                                    if (!this.SendMessage(new AuthReply() { Status = true, Reason = String.Empty }, out error))
                                    {
                                        this.Close("Sending auth reply failed: " + error);
                                        return;
                                    }
                                }
                                catch (Exception exp)
                                {
                                    this.Log(exp, "Exception occurred when auth. message received: {0}", exp.Message);
                                    this.Close("Exception when auth. message received");
                                }
                            }
                        } break;
                    #endregion
                }
            }
            catch (Exception exp) { this.Log(exp, "Exception while parsing and processing message: {0}", exp.Message); }
        }

        /// <summary>
        /// Sending message to service client
        /// </summary>
        /// <param name="messageBase">Message</param>
        /// <param name="error">Error message</param>
        /// <returns>Message sent or not</returns>
        private bool SendMessage(MessageBase messageBase, out String error)
        {
            error = String.Empty;

            try
            {
                String messageData = messageBase.Encode();
                this.Log("Sending message: {0} to client", messageBase.MessageType.ToString());

                this.SendEncodedMessage(messageData);
                return true;
            }
            catch (Exception exp)
            {
                error = String.Format("Exception while sending: {0} to client. {1}", messageBase.MessageType.ToString(), exp.Message);
                this.Log(exp, error);
            }

            return false;
        }
        #endregion

        #region INTERNAL METHODS
        /// <summary>
        /// Queues the received message from service client
        /// </summary>
        /// <param name="message"></param>
        internal void QueueReceivedMessage(String message) 
        { 
            lock (this.messagesQueue) 
            { 
                this.messagesQueue.Enqueue(message); 
            } 
        }

        /// <summary>
        /// Sends frame to service client
        /// </summary>
        /// <param name="e"></param>
        internal void SendFrame(FrameReceivedArgs e)
        {
            String error = String.Empty;
            switch (e.FrameType)
            {
                case FrameReceivedArgs.FrameReceivedType.Video:
                    {
                        this.SendMessage(
                            new ColorImage() { Image = e.Bitmap, KinectDevice = e.KinectDevice, Resoloution = e.Resolution }, out error);
                    } break;
                case FrameReceivedArgs.FrameReceivedType.Depth:
                    {
                        this.SendMessage(
                            new DepthImage()
                            {
                                Image = e.Bitmap,
                                KinectDevice = e.KinectDevice,
                                Resoloution = e.Resolution,
                                BytesPerPixel = e.BytesPerPixel,
                                Width = e.Width,
                                Height = e.Height,
                                ImageBitData = e.ImageByteData
                            }, out error);
                    } break;
            }
        }

        /// <summary>
        /// Sends skeleton data to service client
        /// </summary>
        /// <param name="skeletonData"></param>
        internal void SendSkeletonData(SkeletonData skeletonData)
        {
            String error = String.Empty;

            skeletonData.JointsToSend = this.SkeletonJointsToSend;
            this.SendMessage(skeletonData, out error);
        }

        /// <summary>
        /// Sends encoded message to service client
        /// </summary>
        /// <param name="encodedMessage"></param>
        internal void SendEncodedMessage(String encodedMessage) 
        {
            this.session.SendResponseAsync(encodedMessage); 
        }

        /// <summary>
        /// Terminates the service client session
        /// </summary>
        /// <param name="reason">Reason for closing session</param>
        internal void Close(String reason)
        {
            ConnectionStatus statusWas;
            lock (this._CloseSync)
            {
                statusWas = this.ConnectionStatus;
                this.ConnectionStatus = ConnectionStatus.Disconnected;
            }

            if (statusWas == ConnectionStatus.Disconnected) return;

            this.Log("Closing connection: {0}", reason);
            this.session.Close(this.serverWrapper.Stopping ? CloseReason.ServerShutdown : CloseReason.ServerClosing, reason);
        }

        #region LOGGING METHODS
        /// <summary>
        /// Writes log
        /// </summary>
        /// <param name="logFormat">Log format</param>
        /// <param name="args">Arguments</param>
        internal void Log(String logFormat, params Object[] args) { this.Log(null, logFormat, args); }

        /// <summary>
        /// Writes log
        /// </summary>
        /// <param name="exp">Exception</param>
        /// <param name="logFormat">Log format</param>
        /// <param name="args">Arguments</param>
        internal void Log(Exception exp, String logFormat, params Object[] args)
        {
            this.serverWrapper.Log(exp, "[{0}@{1}] >> {2}", this.UserName, this.RemoteEndPoint, String.Format(logFormat, args));
        }
        #endregion

        #endregion

        #region PUBLIC METHODS
        /// <summary>
        /// Dispose object
        /// </summary>
        public void Dispose() { this.Close("Object Dispose call"); }
        #endregion
    }
}
